package org.xteam.box2text;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

import org.eclipse.emf.common.util.EList;
import org.xteam.box2text.box.AOperator;
import org.xteam.box2text.box.Align;
import org.xteam.box2text.box.AlignmentOption;
import org.xteam.box2text.box.Box;
import org.xteam.box2text.box.BoxFactory;
import org.xteam.box2text.box.HOVOperator;
import org.xteam.box2text.box.HOperator;
import org.xteam.box2text.box.HVOperator;
import org.xteam.box2text.box.IOperator;
import org.xteam.box2text.box.Operator;
import org.xteam.box2text.box.OperatorKinds;
import org.xteam.box2text.box.Option;
import org.xteam.box2text.box.ROperator;
import org.xteam.box2text.box.TextBox;
import org.xteam.box2text.box.VOperator;
import org.xteam.box2text.box.util.BoxSwitch;

public class BoxToText extends BoxSwitch<TextStruct> {
	
	private static class PlaceContext {

		private Frame frame;
		private TextStruct textStruct;
		private OperatorKinds context;

		public PlaceContext(Frame frame, TextStruct textStruct, OperatorKinds context) {
			this.frame = frame;
			this.textStruct = textStruct;
			this.context = context;
		}
		
	}
	
	private Stack<PlaceContext> stack = new Stack<PlaceContext>();

	/**
	 * Compute the text representation of this box hierarchy
	 * 
	 * @return the result text
	 */
	public Text text(Box box) {
        return text(box, new Frame()); 
	}
	
	/**
	 * Compute the text representation of this box hierarchy
	 * 
	 * @param rightMargin
	 * @return the result text
	 */
	public Text text(Box box, int rightMargin) {
		Frame f = new Frame();
		f = f.setRightMargin(rightMargin);
		return text(box, f);
	}
	
	private Text text(Box box, Frame f) {
		RemoveGroups rm = new RemoveGroups();
		List<Box> boxes = rm.doSwitch(box);
		if (boxes.size() > 1) {
			throw new RuntimeException("multiple root boxes");
		}
        TextStruct ts = doPlace(box, f, new TextStruct(new Text(), f.point()), OperatorKinds.V);
        return ts.text(); 
	}
	
	private TextStruct doPlace(Box box, Frame f, TextStruct textStruct, OperatorKinds context) {
		stack.push(new PlaceContext(f, textStruct, context));
        TextStruct ts = doSwitch(box);
        stack.pop();
        return ts;
	}
	
	public TextStruct caseTextBox(TextBox box) {
		Frame frame = stack.peek().frame;
		TextStruct textStruct = stack.peek().textStruct;
		if (frame.y() != textStruct.y()) {
			textStruct = new TextStruct(textStruct.text().startNewLine(), new Point(0, textStruct.y() + 1));
		}
		Text t = textStruct.text().addToLastLine(new Line(frame.x() - textStruct.x()).concat(new Line(box.getContents())));
		return new TextStruct(t, new Point(frame.x() + box.getContents().length(), frame.y()));
	}
	
	public TextStruct caseVOperator(VOperator box) {
		Frame frame = stack.peek().frame;
		TextStruct textStruct = stack.peek().textStruct;
		if (box.getArguments().isEmpty())
			return textStruct;
		if (box.getArguments().size() == 1) {
			return doPlace(box.getArguments().get(0), frame, textStruct, OperatorKinds.V);
		}
		int Vs = frame.getVs(box.getOptions());
		return vplace(box.getArguments(), Vs, frame.setLeftMargin(frame.x()), textStruct);
	}
	
	protected TextStruct vplace(List<Box> boxes, int Vs, Frame f, TextStruct textStruct) {
		for (Box b : boxes) {
			textStruct = doPlace(b, f, textStruct, OperatorKinds.V);
			f = f.vsep(textStruct.point(), Vs);
		}
		return textStruct;
	}
	
	public TextStruct caseHOperator(HOperator box) {
		Frame frame = stack.peek().frame;
		TextStruct textStruct = stack.peek().textStruct;
		return makeHorizontal(box, box.getOptions(), frame, textStruct);
	}
	
	private TextStruct makeHorizontal(Operator box, EList<Option> options, Frame frame, TextStruct textStruct) {
		if (box.getArguments().isEmpty())
			return textStruct;
		if (box.getArguments().size() == 1) {
			return doPlace(box.getArguments().get(0), frame /*.setTs(options())*/, textStruct, OperatorKinds.H);
		}
		int Hs = frame.getHs(options);
		return hplace(box.getArguments(), Hs, frame/*.setTs(options())*/, textStruct);
	}
	
	protected TextStruct hplace(List<Box> boxes, int Hs, Frame f, TextStruct textStruct) {
		for (Box b : boxes) {
			textStruct = doPlace(b, f, textStruct, OperatorKinds.H);
			f = f.hsep(textStruct.point(), Hs);
		}
		return textStruct;
	}
	
	public TextStruct caseIOperator(IOperator box) {
		Frame frame = stack.peek().frame;
		TextStruct textStruct = stack.peek().textStruct;
		OperatorKinds context = stack.peek().context;
		if (context == OperatorKinds.V) {
			int Is = frame.getIs(box.getOptions());
			return makeHorizontal(box, box.getOptions(), frame.setPoint(frame.x()+Is,frame.y()), textStruct);
			//return doPlace(makeHOperator(box.getHorizontalSpace(), box.getArguments()),
			//		frame.setPoint(frame.x()+Is,frame.y()), textStruct, OperatorKinds.V);
		}
		return makeHorizontal(box, box.getOptions(), frame, textStruct);
		//return doPlace(makeHOperator(box.getHorizontalSpace(), box.getArguments()), frame, textStruct, context);
	}
	
	public TextStruct caseAOperator(AOperator box) {
		Frame frame = stack.peek().frame;
		TextStruct textStruct = stack.peek().textStruct;
		if (box.getArguments().isEmpty())
			return textStruct;
		AOperator na = box; //new AOperator(options(), box.getAlignmentOptions(), removeVertical(box.getArguments(), false));
		AlignInfo alignInfo = calcWidth(na, frame);
		return preAlign(na, frame, textStruct, alignInfo);
	}
	
	private AlignInfo calcWidth(AOperator a, Frame f) {
		AlignInfo info = new AlignInfo();
		for (int i = 0; i < a.getAlignmentOptions().size(); ++i) {
			List<Box> col = getColumn(a, i);
			int width = new BoxWidth(f).verticalWidth(col);
			Point bp = new BoxLength(f, OperatorKinds.H).vlength(col);
			if (bp.width() < width) {
				info.add(new Point(width, width));
			} else {
				info.add(new Point(width, bp.width()));
			}
		}
		return info;
	}
	
	private List<Box> getColumn(AOperator tab, int col) {
		List<Box> result = new ArrayList<Box>();
		for (Box row : tab.getArguments()) {
			if (row instanceof ROperator) {
				ROperator r = (ROperator) row;
				if (r.getArguments().size() <= col)
					result.add(makeTextBox(""));
				else
					result.add(r.getArguments().get(col));
			} else {
				result.add(row);
			}
		}
		return result;
	}
	
	private TextStruct preAlign(AOperator na, Frame frame, TextStruct textStruct, AlignInfo alignInfo) {
		int Hs = frame.getHs(na.getOptions());
        int availableSpace = frame.getRightMargin() - textStruct.x();
        if (availableSpace >= alignInfo.maximalWidth(Hs)) {
			alignInfo.fixColumnWidthsMaximal();
		} else if (availableSpace < alignInfo.minimalWidth(Hs)) {
        	alignInfo.fixColumnWidthsMinimal();
        } else {
        	alignInfo.fixColumnWidths(Hs, availableSpace);
        }
        Frame f = frame.setLeftMargin(frame.x());
        return align(na, f, textStruct, alignInfo);
	}
	
	private TextStruct align(AOperator na, Frame frame, TextStruct textStruct, AlignInfo alignInfo) {
		int Vs = frame.getVs(na.getOptions());
		for (Box box : na.getArguments()) {
			textStruct = align((ROperator)box, na /* for options */, frame, textStruct, alignInfo);
			frame = frame.vsep(textStruct.point(), Vs);
		}
		return textStruct;
	}
	
	private TextStruct align(ROperator row, AOperator na, Frame frame,
			TextStruct textStruct, AlignInfo alignInfo) {
		for (int i = 0; i < row.getArguments().size(); ++i) {
			AlignmentOption ao = na.getAlignmentOptions().get(i);
			int width = alignInfo.getWidth(i);
			Frame f = frame.setPoint(0, 0).setRightMargin(width).setLeftMargin(0);
			TextStruct cellText = doPlace(row.getArguments().get(i), f,
					new TextStruct(new Text(), new Point(0, 0)), OperatorKinds.H);
			if (ao.getType() == Align.L) {
				cellText = cellText.alignLeft(f);
			} else if (ao.getType() == Align.R) {
				cellText = cellText.alignRight(f);
			} else if (ao.getType() == Align.C) {
				cellText = cellText.alignCenter(f);
			} else {
				throw new RuntimeException("unknown alignment option");
			}
			textStruct = textStruct.concat(cellText, frame);
			frame = frame.hsep(textStruct.point(), frame.getHs(na.getOptions()));
		}
		return textStruct;
	}
	
	public TextStruct caseHOVOperator(HOVOperator box) {
		Frame frame = stack.peek().frame;
		TextStruct textStruct = stack.peek().textStruct;
		OperatorKinds context = stack.peek().context;
		if (box.getArguments().isEmpty())
			return textStruct;
		if (box.getArguments().size() == 1)
			return doPlace(box.getArguments().get(0), frame, textStruct, context);

		Point bp = BoxLength.length(box, frame, OperatorKinds.H);
		if (bp.height() == 0 && (frame.x() + bp.width()) <= frame.getRightMargin()) {
			return hplace(box.getArguments(), frame.getHs(box.getOptions()), frame, textStruct);
		}
		Frame f = frame.setLeftMargin(frame.x());
		return vplace(box.getArguments(), frame.getVs(box.getOptions()), f, textStruct);
	}
	
	public TextStruct caseHVOperator(HVOperator box) {
		Frame frame = stack.peek().frame;
		TextStruct textStruct = stack.peek().textStruct;
		OperatorKinds context = stack.peek().context;
		if (box.getArguments().isEmpty())
			return textStruct;
		
		if (box.getArguments().size() == 1)
			return doPlace(box.getArguments().get(0), frame, textStruct, context);

		//BoxPoint bp = length(frame, OperatorKinds.H);
		int Hs = frame.getHs(box.getOptions());
		int Vs = frame.getVs(box.getOptions());
		int Is = frame.getIs(box.getOptions());
		return hvplace(box.getArguments(),
				Hs, Vs, Is, frame.setLeftMargin(frame.x()), textStruct);
	}

	private TextStruct hvplace(List<Box> boxes, int hs, int vs, int is,
			Frame frame, TextStruct textStruct) {
		boolean isFirst = true;
		for (Box box : boxes) {
			Point bp = BoxLength.length(box, frame, OperatorKinds.H);
			if ( ! bp.isZero()) {
				if (isFirst) {
					isFirst = false;
					textStruct = doPlace(box, frame, textStruct, OperatorKinds.H);
				} else {
					Frame f = frame.hsep(textStruct.point(), hs);
					textStruct = reshuffle(box, bp, f, textStruct, hs, vs, is);
				}
			}
		}
		return textStruct;
	}

	private TextStruct reshuffle(Box box, Point size,
			Frame frame, TextStruct textStruct, int hs, int vs, int is) {
		if (size.y() == 0
				&& (frame.x() + size.x()) <= frame.getRightMargin()) {
			return doPlace(box, frame, textStruct, OperatorKinds.H);
		}
		Frame f = frame.setPoint(frame.getLeftMargin() + is, frame.y() + vs);
        return doPlace(box, f, textStruct, OperatorKinds.V);
	}
	
	private TextBox makeTextBox(String contents) {
		TextBox box = BoxFactory.eINSTANCE.createTextBox();
		box.setContents(contents);
		return box;
	}
	
}
